﻿using System;
using System.Linq;
using System.Xml;
using Notoric.Model.Collections;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;
using Notoric.Model.Validation;

namespace Notoric.Model.Elements.CWNotation
{
    /// <summary>
    /// Implements the Common Western Music Notation <see cref="CustomPartDataElement"/>.
    /// </summary>
    public class CWPartData : CustomPartDataElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="CWPartData"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="PartDescriptorElement"/>
        /// </para>
        /// <para>
        /// The part descriptor that directly owns this element.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        public CWPartData(CompositionPart packagePart, PartDescriptorElement owner)
            : base(packagePart, owner)
        {
            barlineSegments = new ElementCollection<BarlineSegment>(this, barlineSegmentsElement);
            staffConnectors = new ElementCollection<StaffConnectorDescriptor>(this, staffConnectorsElement);
            stavesCollection = new ElementCollection<StaffDescriptor>(this, stavesElement);
            staves = new ReadOnlyCollection<StaffDescriptor>(stavesCollection);
        }

        /// <summary>
        /// Creates a new <see cref="CWPartData"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="PartDescriptorElement"/>
        /// </para>
        /// <para>
        /// The part descriptor that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        public CWPartData(CompositionPart packagePart, PartDescriptorElement owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            stavesElement = element[PackageVocabulary.StavesElementName, PackageVocabulary.PackageNamespaceUri];

            stavesCollection = new ElementCollection<StaffDescriptor>(this, stavesElement);
            staves = new ReadOnlyCollection<StaffDescriptor>(stavesCollection);

            barlineSegmentsElement = element[PackageVocabulary.BarlineSegmentElementName + "s", PackageVocabulary.PackageNamespaceUri];
            barlineSegments = new ElementCollection<BarlineSegment>(this, barlineSegmentsElement);

            staffConnectorsElement = element[PackageVocabulary.StaffConnectorElementName + "s", PackageVocabulary.PackageNamespaceUri];
            staffConnectors = new ElementCollection<StaffConnectorDescriptor>(this, staffConnectorsElement);
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.CWPartDataElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            barlineSegmentsElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.BarlineSegmentElementName + "s",
                PackageVocabulary.PackageNamespaceUri
                );

            staffConnectorsElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.StaffConnectorElementName + "s",
                PackageVocabulary.PackageNamespaceUri
                );

            stavesElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.StavesElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            element.AppendChild(barlineSegmentsElement);
            element.AppendChild(staffConnectorsElement);
            element.AppendChild(stavesElement);

            return element;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates a new <see cref="BarlineSegment"/> and adds it to the <see cref="BarlineSegments"/> collection.
        /// </summary>
        /// <remarks>
        /// To create a segment that is drawn on a single staff, set both arguments to the same descriptor instance.
        /// </remarks>
        /// <param name="upperStaff">
        /// <para>
        /// Type: <see cref="StaffDescriptor"/>
        /// </para>
        /// <para>
        /// The descriptor of the upper staff of the segment.
        /// </para>
        /// </param>
        /// <param name="lowerStaff">
        /// <para>
        /// Type: <see cref="StaffDescriptor"/>
        /// </para>
        /// <para>
        /// The descriptor of the lower staff of the segment.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="BarlineSegment"/>
        /// </para>
        /// <para>
        /// The created barline segment.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ValidationException">
        /// <paramref name="upperStaff"/> or <paramref name="lowerStaff"/> does not belong to this part descriptor.
        /// </exception>
        public BarlineSegment AddBarlineSegment(StaffDescriptor upperStaff, StaffDescriptor lowerStaff)
        {
            var segment = new BarlineSegment(PackagePart, this, upperStaff, lowerStaff);
            barlineSegments.Add(segment);

            return segment;
        }

        /// <summary>
        /// Adds a new staff connector.
        /// </summary>
        /// <param name="connectorClass">
        /// <para>
        /// Type: <see cref="Type"/>
        /// </para>
        /// <para>
        /// The connector class. It must be a subclass of <see cref="StaffConnector"/>.
        /// </para>
        /// </param>
        /// <param name="lowerStaff">
        /// <para>
        /// Type: <see cref="StaffDescriptor"/>
        /// </para>
        /// <para>
        /// The lower staff.
        /// </para>
        /// </param>
        /// <param name="upperStaff">
        /// <para>
        /// Type: <see cref="StaffDescriptor"/>
        /// </para>
        /// <para>
        /// The upper staff.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="StaffConnectorDescriptor"/>
        /// </para>
        /// <para>
        /// The created connector descriptor.
        /// </para>
        /// </returns>
        /// <exception cref="ValidationException">
        /// The value supplied for some argument is not valid.
        /// Please refer to the documentation of the <see cref="StaffConnectorDescriptor"/> for details.
        /// </exception>
        public StaffConnectorDescriptor AddStaffConnector(Type connectorClass, StaffDescriptor lowerStaff, StaffDescriptor upperStaff)
        {
            var connector = new StaffConnectorDescriptor(PackagePart, this)
            {
                ConnectorClass = connectorClass,
                LowerStaff = lowerStaff,
                UpperStaff = upperStaff
            };

            staffConnectors.Add(connector);

            return connector;
        }

        /// <summary>
        /// Creates a new <see cref="StaffDescriptor"/> and adds it to the <see cref="Staves"/> collection.
        /// </summary>
        /// <param name="lines">
        /// <para>
        /// Type: <see cref="Byte"/>
        /// </para>
        /// <para>
        /// The number of staff lines.
        /// </para>
        /// </param>
        /// <param name="index">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The index of the staff within the part.
        /// </para>
        /// </param>
        /// <param name="height">
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The default height (in centimeters) of the staff.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="StaffDescriptor"/>
        /// </para>
        /// <para>
        /// The created descriptor.
        /// </para>
        /// </returns>
        public StaffDescriptor AddStaffDescriptor(byte lines, int index, double height)
        {
            var descriptor = new StaffDescriptor(PackagePart, this);
            descriptor.Lines = lines;
            descriptor.DefaultHeight = height;
            stavesCollection.Insert(index, descriptor);

            return descriptor;
        }

        /// <summary>
        /// Reinserts an existing staff descriptor.
        /// </summary>
        /// <param name="descriptor">
        /// <para>
        /// Type: <see cref="StaffDescriptor"/>
        /// </para>
        /// <para>
        /// The descriptor.
        /// </para>
        /// </param>
        /// <param name="index">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The index in the <see cref="Staves"/> collection where the descriptor will be inserted.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="descriptor"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="descriptor"/> does not belong to this part.<br/>
        /// -or-<br/>
        /// The <paramref name="descriptor"/> is already present in this part.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// The <paramref name="index"/> is less than 0 or greater than the number of staves - 1.
        /// </exception>
        public void AddExistingStaffDescriptor(StaffDescriptor descriptor, int index)
        {
            if (descriptor == null)
                throw new ArgumentNullException("descriptor");
            if (descriptor.FindOwner<CWPartData>() != this)
                throw new ArgumentException("The descriptor does not belong to this part.", "descriptor");
            if (index < 0 || index >= stavesCollection.Count)
                throw new ArgumentOutOfRangeException("index");
            if (stavesCollection.Contains(descriptor))
                throw new ArgumentException("The descriptor of this staff is already present.", "descriptor");

            stavesCollection.Insert(index, descriptor);
        }

        /// <summary>
        /// Removes a staff descriptor.
        /// </summary>
        /// <param name="descriptor">
        /// <para>
        /// Type: <see cref="StaffDescriptor"/>
        /// </para>
        /// <para>
        /// The descriptor.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="descriptor"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="descriptor"/> does not belong to this part.
        /// </exception>
        public void RemoveStaffDescriptor(StaffDescriptor descriptor)
        {
            if (descriptor == null)
                throw new ArgumentNullException("descriptor");
            if (descriptor.FindOwner<CWPartData>() != this)
                throw new ArgumentException("The descriptor does not belong to this part.", "descriptor");

            stavesCollection.Remove(descriptor);
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the collection of the barline segments.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ElementCollection{T}"/> of <see cref="BarlineSegment"/>
        /// </para>
        /// <para>
        /// The collection of barline segments.<br/>
        /// Barline segments define how a barline (in how many segments) is drawn on this part.
        /// </para>
        /// </value>
        public ElementCollection<BarlineSegment> BarlineSegments
        {
            get { return barlineSegments; }
        }

        /// <summary>
        /// Gets the staff connectors.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ElementCollection{T}"/> of <see cref="StaffConnectorDescriptor"/>
        /// </para>
        /// <para>
        /// The collection of descriptors of the connectors of the staves belonging to this part.
        /// </para>
        /// </value>
        public ElementCollection<StaffConnectorDescriptor> StaffConnectors
        {
            get { return staffConnectors; }
        }

        /// <summary>
        /// Gets the collection of staves in this part.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ReadOnlyCollection{T}"/> of <see cref="StaffDescriptor"/>
        /// </para>
        /// <para>
        /// The collection of staves to which the part is notated in the common western music notation.
        /// </para>
        /// </value>
        public ReadOnlyCollection<StaffDescriptor> Staves
        {
            get { return staves; }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the <see cref="BarlineSegments"/> collection.
        /// </summary>
        private ElementCollection<BarlineSegment> barlineSegments;
        /// <summary>
        /// Holds the XML element that holds the <see cref="barlineSegments"/> collection.
        /// </summary>
        private XmlElement barlineSegmentsElement;
        /// <summary>
        /// Holds the <see cref="StaffConnectors"/> collection.
        /// </summary>
        private ElementCollection<StaffConnectorDescriptor> staffConnectors;
        /// <summary>
        /// Holds the XML element that holds the <see cref="StaffConnectors"/> collection.
        /// </summary>
        private XmlElement staffConnectorsElement;
        /// <summary>
        /// Holds the value of the <see cref="Staves"/> property.
        /// </summary>
        private ReadOnlyCollection<StaffDescriptor> staves;
        /// <summary>
        /// Holds the collection of staves.
        /// </summary>
        private ElementCollection<StaffDescriptor> stavesCollection;
        /// <summary>
        /// Holds the XML element that holds the <see cref="stavesCollection"/>.
        /// </summary>
        private XmlElement stavesElement;

        #endregion
    }
}
